import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { Inject, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import * as argon2 from "argon2";
import { Captcha, resolveImage } from "captcha-canvas";
import { plainToInstance } from "class-transformer";
import Redis from "ioredis";
import { ResultMsg } from "@/common/enum/result-msg.enum";
import Result from "@/common/utils/result.util";
import { REDIS_CLIENT } from "@/database/redis.module";
import { MenuService } from "@/modules/system/menu/menu.service";
import { PermService } from "@/modules/system/permission/perm.service";
import { UserService } from "@/modules/system/user/user.service";
import { UserBaseVo } from "@/modules/system/user/vo/user-base.vo";
import { SignInDto } from "./dto/sign-in.dto";

const accessTokenExpires = 20 * 60; // 默认访问 token 过期时间为 20 分钟，单位秒 (注意这里前端做了判断, 不要少于前端请求的超时时间)
const refreshTokenExpires = 24 * 60 * 60; // 默认刷新 token 过期时间为 24 小时，单位秒

@Injectable()
export class AuthService {
    constructor(
        @Inject(REDIS_CLIENT) private redis: Redis,
        private userService: UserService,
        private menuService: MenuService,
        private permService: PermService,
        private jwtService: JwtService,
    ) {}

    /** 刷新token */
    async refreshAccessToken(refreshToken: string): Promise<Result<Token>> {
        try {
            await this.redis.select(1);
            // 验证完成后返回元信息
            const payload = this.jwtService.verify(refreshToken);

            // 查看黑名单中是否有记录
            if (await this.redis.get(`rt_black:${payload.jti}`)) {
                throw new Error(); // 会执行 catch 语句
            }

            // 立即作废旧 refreshToken
            const rtTTL = payload.exp - Math.floor(Date.now() / 1000); // 设置过期时间
            await this.redis.setex(`rt_black:${payload.jti}`, rtTTL, "1"); // 存储到黑名单中

            // 生成新的 token
            const token: Token = {
                accessToken: this.jwtService.sign(setTokenPayload(payload), {
                    expiresIn: accessTokenExpires,
                }),
                refreshToken: this.jwtService.sign(setTokenPayload(payload), {
                    expiresIn: payload.exp ? `${(payload.exp * 1000 - Date.now()) / 1000}s` : "7d",
                }),
                expires: Date.now() + accessTokenExpires * 1000,
            };

            return Result.success(token);
        } catch {
            throw new UnauthorizedException();
        }
    }

    /** 账号密码登录 */
    async signInByUsername(signInDto: SignInDto): Promise<Result<UserBaseVo | null>> {
        const { username, password, captchaId, captchaText, rememberDays } = signInDto;
        const user = await this.userService.findByUsername(username);

        if (!user) {
            return Result.error(ResultMsg.LOGIN_NO_USER);
        }

        // 校验验证码
        const isCaptchaValid = await this.verifyCaptcha(captchaId, captchaText);
        if (!isCaptchaValid) {
            return Result.error(ResultMsg.LOGIN_INVALID_CAPTCHA);
        }

        // 验证密码
        if (!(await argon2.verify(user.password ?? "", password))) {
            return Result.error(ResultMsg.LOGIN_INVALID_PASSWORD);
        }

        // 验证当前用户角色
        if (!user.roles || user.roles.length === 0) {
            return Result.error(ResultMsg.LOGIN_NO_ROLES);
        }

        // 生成 JWT
        const token: Token = {
            accessToken: this.jwtService.sign(setTokenPayload(user), {
                expiresIn: accessTokenExpires,
            }),
            refreshToken: this.jwtService.sign(setTokenPayload(user), {
                expiresIn: rememberDays ? `${rememberDays}d` : refreshTokenExpires,
            }),
            expires: Date.now() + accessTokenExpires * 1000,
        };

        // 转换返回数据
        const userVo = plainToInstance(
            UserBaseVo,
            { ...user, ...token },
            { excludeExtraneousValues: true, ignoreDecorators: true },
        );
        return Result.success(userVo, ResultMsg.LOGIN_SUCCESS);
    }

    /** 发送验证码图片 */
    async generateCaptcha() {
        const captchaId = randomUUID();
        const background = await resolveImage(join(process.cwd(), "src/assets/captcha-bg.svg"));

        // 生成验证码图片
        const captcha = new Captcha(120, 40, 4);
        captcha.async = false;
        captcha.drawImage(background);

        // 计算水平居中的坐标
        const coordinates: number[][] = [];
        const widthGap = Math.floor(120 / 4);
        for (let i = 0; i < 4; i++) {
            const x = widthGap * (i + 0.5);
            const y = Math.floor(Math.random() * 20) + 10; // 垂直居中范围
            coordinates.push([x, y]);
        }
        (captcha as any)._coordinates = coordinates;

        captcha.addDecoy({ total: Math.floor((120 * 40) / 2500) });
        captcha.drawCaptcha({
            size: 24,
            skew: false,
            colors: ["#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#e67e22", "#e74c3c"],
        });
        captcha.drawTrace({ color: "#34495e", size: 2, opacity: 0.5 });
        captcha.addDecoy({ opacity: 1 });

        const image = captcha.png;
        const text = captcha.text;

        // 存入缓存，5分钟过期
        await this.redis.select(2);
        await this.redis.setex(`captcha:${captchaId}`, 300, text);

        return Result.success({
            captchaId,
            image: `data:image/png;base64,${image.toString("base64")}`,
        });
    }

    /** 校验登录验证码 */
    async verifyCaptcha(captchaId: string, code: string): Promise<boolean> {
        await this.redis.select(2);
        const cachedCode = await this.redis.get(`captcha:${captchaId}`);
        if (cachedCode && cachedCode.toLowerCase() === code.toLowerCase()) {
            // 验证通过后删除缓存
            await this.redis.del(`captcha:${captchaId}`);
            return true;
        }
        return false;
    }

    /** 根据角色列表获取权限列表 */
    async findPermissions(roles: string[]) {
        const permissions = await this.permService.listByRole(roles);
        const routes = await this.menuService.listRouteTree(roles);
        return Result.success({
            permissions,
            routes,
        });
    }
}

/** 设置访问令牌的有效载荷 */
function setTokenPayload(payload: any) {
    return {
        id: payload.id,
        roles: payload.roles,
        permissions: payload.permissions,
        jti: randomUUID(),
    };
}
